[Android][Framework]A/B system update


简介

A/B系统就是指手机上有A,B两套可用的系统(userdata只有一份)。这两份系统同一时间只运行一个,另外一个作为备份。这两个系统的版本可能一样,也可能不一样。

这里称为一套(slot)系统,是因为Android系统由多个分区组成。对于A/B,分为system_a,vendor_a,boot_a和system_b,vendor_b,boot_b。所以一套就是一组完整的分区。

优势

  • 由于手机上有两套可以正常工作的系统,升级系统时总是存在一个可以运行的系统,减少手机变砖的可能性
  • OTA在系统后台进行,所以更新时用户可以正常使用设备,升级完成后只需要重启一次进入新系统即可
  • 更新后重启系统的时间不会超过常规重启时间(传统OTA需要在recovery中升级,重启过程比较慢)
  • 如果更新的系统无法启动,设备将重启回到旧系统,然后尝试再次更新
  • 任何错误(例如 I/O 错误)都只会影响未使用的分区组,并且用户可以进行重试。由于 I/O 负载被特意控制在较低水平,以免影响用户体验,因此发生此类错误的可能性也会降低
  • 更新包可以流式传输到 A/B 设备,因此在安装之前不需要先下载更新包。流式更新意味着用户没有必要在 /data/cache 上留出足够的可用空间来存储更新包
  • cache分区不再用于存储 OTA 更新包,因此无需确保缓存分区的大小要足以应对日后的更新
  • dm-verity 可保证设备将使用未损坏的启动映像。如果设备因 OTA 错误或 dm-verity 问题而无法启动,则可以重新启动到旧img

与传统OTA差别

  1. 系统的分区设置
    • 传统方式只有一套分区
    • A/B系统有两套分区,称为slot Aslot B
  2. 跟bootloader沟通的方式
    • 传统方式bootloader通过读取misc分区信息来决定是进入Android主系统还是Recovery系统
    • A/B系统的bootloader通过特定的分区信息来决定从slot A还是slot B启动
  3. 系统的编译过程
    • 传统方式在编译时会生成boot.imgrecovery.img分别用于Android主系统和Recovery系统的ramdisk
    • A/B系统只有boot.img,而不再生成单独的recovery.img
  4. OTA更新包的生成方式
    • A/B系统生成OTA包的工具和命令跟传统方式一样,但是生成内容的格式不一样了

分区具体差别:

传统分区

  • bootloader

    存放用于引导linux的bootloader

  • boot

    存放Android主系统linux kernel文件和用于挂载system和其他分区的ramdisk

  • system

    Android 主系统分区,包括Android 系统App和库文件

  • vendor

    Android主系统分区,主要是包含开发厂商定制的一些应用和库文件。tremble了解一下

  • userdata

    用户数据分区,存放用户数据,包括用户安装的App和使用时生成的数据

  • cache

    临时数据分区,通常存放OTA升级包

  • recovery

    存放recovery系统的linux kernel文件和ramdisk

  • misc

    存放Android主系统和Recovery系统跟bootloader通信的数据

A/B system分区

  • bootloader

    存放用于引导linux的bootloader

  • boot_a和boot_b

    分别用于存放两套系统各自的linux kernel文件和用于挂载system和其他分区的ramdisk

  • system_a和system_b

    Android主系统分区,分别用于存放两套系统各自的系统App和库文件

  • vendor_a和vendor_b

    Android主系统分区,分别用于存放两套系统各自开发厂商定制的一些应用和库文件,很多时候开发厂商也直接将这个分区的内容直接放入system分区

  • userdata

    用户数据分区,存放用户数据,包括用户安装的App和使用时生成的数据

  • misc或其他名字分区

    存放Android主系统和Recovery系统跟bootloader通信的数据,由于存放方式和分区名字没有强制要求,所以部分实现上保留了misc分区(代码中可见BrilloIntel的平台),另外部分实现采用其他分区存放数据(Broadcom机顶盒平台采用名为eio的分区)。

区别:

  1. boot, system, vendor变成两套分区,slot A和slot B
  2. 不再需要cache和recovery分区
  3. misc分区不是必须的

更新流程

可以参考代码 boot_control.h

  1. 通过 markBootSuccessful() 将当前slot(或“源slot”)标记为成功(如果尚未标记)
  2. 调用函数 setSlotAsUnbootable(),将未使用的slot(或“目标slot”)标记为不可启动。当前slot始终会在更新开始时被标记为成功,以防止引导加载程序回退到未使用的slot(该slot中很快将会有无效数据)。如果系统已做好准备,可以开始应用更新,那么即使其他主要组件出现损坏(例如界面陷入崩溃循环),当前slot也会被标记为成功,因为可以通过推送新软件来解决这些问题
    • 元数据。元数据在更新有效负载中所占的比重相对较小,其中包含一系列用于在目标slot上生成和验证新版本的操作。例如,某项操作可能会解压缩特定 Blob 并将其写入到目标分区中的特定块,或者从源分区读取数据、应用二进制补丁程序,然后写入到目标分区中的特定块
    • 额外数据。与操作相关的额外数据在更新有效负载中占据了大部分比重,其中包含这些示例中的已压缩 Blob 或二进制补丁程序
  3. 下载有效负载元数据
  4. 对于元数据中定义的每项操作,都将按顺序发生以下行为:将相关数据(如果有)下载到内存中、应用操作,然后释放关联的内存
  5. 对照预期的哈希重新读取并验证所有分区
  6. 运行安装后步骤(如果有)。如果在执行任何步骤期间出现错误,则更新失败,系统可能会通过其他有效负载重新尝试更新。如果上述所有步骤均已成功完成,则更新成功,系统会执行最后一个步骤
  7. 调用 setActiveBootSlot(),将未使用的槽位标记为活动槽位。将未使用的槽位标记为活动槽位并不意味着它将完成启动。如果引导加载程序(或系统本身)未读取到“成功”状态,则可以将活动槽位切换回来
  8. 安装后步骤(如下所述)包括从“新更新”版本中运行仍在旧版本中运行的程序。如果此步骤已在 OTA 更新包中定义,则为强制性步骤,且程序必须返回并显示退出代码 0,否则更新会失败
  9. 在系统足够深入地成功启动到新槽位并完成重新启动后检查之后,系统会调用 markBootSuccessful(),将现在的当前槽位(原“目标槽位”)标记为成功

用具体的示例介绍流程:

如图,每一个slot都有3个属性:

  • active

    系统活动分区标识。同时只有一个slot是active状态,启动时就会启动标识为此的slot

  • bootable

    分区可启动标识。有此标识说明该分区包含一个完整可启动的系统

  • successful

    分区运行成功标识。说明该分区在上次或者当前启动可以正常运行

图中4个场景如下

  1. 普通场景(Normal cases

    最常见的情形,例如设备出厂时,A分区和B分区都可以成功启动并正确运行,所以两个分区都设置为bootablesuccessful,但由于是从B分区启动,所以只有B分区设置为active

  2. 升级中(Update in progress

    B分区检测到升级数据,在A分区进行升级,此时将A分区标识为unbootable,另外清除successful标识;B分区仍然为activebootablesuccessful

  3. 更新完成,等待重启(Update applied, reboot pending

    B分区将A分区成功更新后,将A分区标识为bootable。另外,由于重启后需要从A分区启动,所以也需要将A分区设置为active,但是由于还没有验证过A分区是否能成功运行,所以不设置successful;B分区的状态变为bootablesuccessful,但没有active

  4. 从新系统成功启动(System rebooted into new update

    设备重启后,bootloader检测到A分区为active,所以加载A分区系统。进入A系统后如果能正确运行,需要将A分区标识为successful。对比第1个普通场景,A和B系统都设置为bootablesuccessful,但active从B分区切换到A分区。至此,B分区成功更新并切换到A分区,设备重新进入普通场景。

分区烧写

首先A/B系统和传统OTA系统只能存在一个,在编译时选择。烧写A/B系统也和烧正常系统一样,指定ab分区即可。

target reported max download size of 536870912 bytes
erasing 'system_a'...
OKAY [  0.023s]
sending sparse 'system_a' 1/7 (516186 KB)...
OKAY [ 17.114s]
writing 'system_a' 1/7...

target reported max download size of 536870912 bytes
sending sparse 'system_b' 1/7 (516186 KB)...
OKAY [ 16.368s]
writing 'system_b' 1/7...

Java接口使用方法

Android提供了Java层的接口,UpdateEngine.java

接口很好用

  1. 首先创建一个UpdateEngine的实例
  2. 实现 UpdateEngineCallback 回调接口
  3. 调用bind方法绑定Callback
  4. 调用applyPayload方法执行更新
@SystemApi
public void applyPayload(String url, long offset, long size, String[] headerKeyValuePairs) {
    try {
        mUpdateEngine.applyPayload(url, offset, size, headerKeyValuePairs);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

这个方法是@SystemApi,调用它有两个方法:

  • 把APK放入源码环境,写Makefile进行编译
  • 独立APK,把系统framework.jar作为lib导入APK。(也可以只把UpdateEngine相关类打包编译)

关于参数,第一个是差分包(zip包)的路径。第四个是差分包内部的一个文件,这个文件由做包工具生成,一般会保存为一个payload_properties.txt文件,文件大概内容如下:

FILE_HASH=lURPCIkIAjtMOyB/EjQcl8zDzqtD6Ta3tJef6G/+z2k=
FILE_SIZE=871903868
METADATA_HASH=tBvj43QOB0Jn++JojcpVdbRLz0qdAuL+uTkSy7hokaw=
METADATA_SIZE=70604

我们要做的就是把文件内容读出来,存在数组里作为第四个参数,形式如下。

String[] pairs = {
    "FILE_HASH=lURPCIkIAjtMOyB/EjQcl8zDzqtD6Ta3tJef6G/+z2k=",
    "FILE_SIZE=871903868",
    "METADATA_HASH=tBvj43QOB0Jn++JojcpVdbRLz0qdAuL+uTkSy7hokaw=",
    "METADATA_SIZE=70604"
};

然后贴一个我用的处理方法:

private String[] readHeaderKeyValuesFromZipFile(String file) {
    String[] values = new String[4];
    try {
        ZipFile zf = new ZipFile(file);
        InputStream in = new BufferedInputStream(new FileInputStream(file));
        ZipInputStream zin = new ZipInputStream(in);
        ZipEntry ze;
        while ((ze = zin.getNextEntry()) != null) {
            if (ze.isDirectory()) {
                //Do nothing
            } else {
                if (ze.getName().contains("payload_properties.txt")) {
                    BufferedReader br = new BufferedReader(
                        new InputStreamReader(zf.getInputStream(ze)));
                    String line;
                    int index = 0;
                    while ((line = br.readLine()) != null) {
                        values[index] = line;
                        index++;
                    }
                    br.close();
                }
            }
        }
        zin.closeEntry();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return values;
}

参考

https://source.android.com/devices/tech/ota/ab/ab_implement

https://blog.csdn.net/guyongqiangx/article/details/71334889


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
 上一篇
[Android][Framework]Activity启动流程 [Android][Framework]Activity启动流程
前面介绍了ActivityThread,就顺带提了一下Activity的启动流程。本来感觉之前的分析已经把启动流程分析了一大半,可结果发现并没有想象的那么简单。关于ActivityManagerService,也是一个庞大的家伙,看了很多资料,结合源码也还是比较困难。因为资料都是基于旧代码的分析,而我是跟着Andorid O的源码去看,所以很多代码对不上,无形中又增加了不少困难。不过这种困难都是小问题,只要耐心,都能跟的下去。因为工作原因,我手里会有很多网上没有的分析资料,涵盖了AMS大大小小的处理问题。所以会对问题做一些梳理,对照着源码加深理解。
2018-06-03
下一篇 
  目录